跳到主要内容

Shell 循环语句

for 循环

for var in item1 item2 ... itemN
do
command1
command2
...
commandN
done

写成一行:

for var in item1 item2 ... itemN; do command1; command2… done;

当变量值在列表里,for 循环即执行一次所有命令,使用变量名获取列表中的当前取值。命令可为任何有效的 shell 命令和语句。in 列表可以包含替换、字符串和文件名。

in列表是可选的,如果不用它,for循环使用命令行的位置参数。

例如,顺序输出当前列表中的数字:

for loop in 1 2 3 4 5
do
echo "The value is: $loop"
done

顺序输出字符串中的字符:

#!/bin/bash

for str in This is a string
do
echo $str
done

输出结果:

This
is
a
string

从变量读取列表

#!/bin/bash
# using a variable to hold the list
list="Alabama Alaska Arizona Arkansas Colorado"
# 给这个 list 添加一个 Connecticut 变量
list=$list" Connecticut"

for state in $list
do
echo "Have you ever visited $state?"
done

输出:

Have you ever visited Alabama?
Have you ever visited Alaska?
Have you ever visited Arizona?
Have you ever visited Arkansas?
Have you ever visited Colorado?
Have you ever visited Connecticut?

遍历数组

## declare an array variable
declare -a arr=("element1" "element2" "element3")

## now loop through the above array
for i in "${arr[@]}"
do
echo "$i"
# or do whatever with individual element of the array
done

# You can access them using echo "${arr[0]}", "${arr[1]}" also

从命令读取值

有时需要从命令里面读取返回值来循环

如下:

创建一个文件 test.txt

Alabama Alaska  Arizona Arkansas
Colorado
Connecticut
Delaware
Florida
Georgia

从这个文件里面读取

#!/bin/bash
# reading values from a file
file="test.txt"
for state in $(cat $file)
do
echo "Visit beautiful $state"
done

输出:

Visit beautiful Alabama
Visit beautiful Alaska
Visit beautiful Arizona
Visit beautiful Arkansas
Visit beautiful Colorado
Visit beautiful Connecticut
Visit beautiful Delaware
Visit beautiful Florida
Visit beautiful Georgia

更改字段分隔符

但是仔细观察可以发现,上面不管是空格还是换行符,在循环中都当一个新的元素打印出来,造成这个问题的原因是特殊的环境变量 IFS,叫作内部字段分隔符(internal field separator)。IFS 环境变量定义了 bash shell 用作字段分隔符的一系列字符。默认情况下,bash shell 会将下列字符当作字段分隔符:

  • 空格
  • 制表符
  • 换行符

如果 bash shell 在数据中看到了这些字符中的任意一个,它就会假定这表明了列表中一个新数据字段的开始。在处理可能含有空格的数据(比如文件名)时,这会非常麻烦,就像上面脚本示例中看到的。

要解决这个问题,可以在 shell 脚本中临时更改 IFS 环境变量的值来限制被 bash shell 当作字段分隔符的字符。

IFS=$'\n'

将这个语句加入到脚本中,告诉 bash shell 在数据值中忽略空格和制表符。

在处理代码量较大的脚本时,可能在一个地方需要修改 IFS 的值,然后忽略这次修改,在脚本的其他地方继续沿用 IFS 的默认值。一个可参考的安全实践是在改变 IFS 之前保存原来的 IFS 值,之后再恢复它。这就保证了在脚本的后续操作中使用的是 IFS 的默认值。

上面的脚本更新为:

#!/bin/bash

IFS_OLD=$IFS
IFS=$'\n'

# reading values from a file
file="test.txt"
for state in $(cat $file)
do
echo "Visit beautiful $state"
done

# 变更回来
IFS=$IFS_OLD

# do somthing...

输出:

Visit beautiful Alabama Alaska  Arizona Arkansas
Visit beautiful Colorado
Visit beautiful Connecticut
Visit beautiful Delaware
Visit beautiful Florida
Visit beautiful Georgia

还有其他一些 IFS 环境变量的绝妙用法。假定你要遍历一个文件中用冒号分隔的值(比如在/etc/passwd 文件中)。你要做的就是将 IFS 的值设为冒号

IFS=:

如果要指定多个 IFS 字符,只要将它们在赋值行串起来就行。

IFS=$'\n':;"

这个赋值会将换行符、冒号、分号和双引号作为字段分隔符。如何使用 IFS 字符解析数据没有任何限制。

用通配符读取目录

可以用 for 命令来自动遍历目录中的文件。进行此操作时,必须在文件名或路径名中使用通配符。它会强制 shell 使用文件扩展匹配。文件扩展匹配是生成匹配指定通配符的文件名或路径名的过程。

#!/bin/bash

for file in "$HOME"/Documents/markdownNote/*
do
if [ -d "$file" ]
then
echo "$file is a directory"
elif [ -f "$file" ]
then
echo "$file is a file"
fi
done

输出:

/home/alsritter/Documents/markdownNote/分布式的理论和算法 is a directory
/home/alsritter/Documents/markdownNote/前端 is a directory
/home/alsritter/Documents/markdownNote/图形学 is a directory
/home/alsritter/Documents/markdownNote/学习计划(随时更新).md is a file

注意,在这个例子的 if 语句中做了一些不同的处理

if [ -d "$file" ]

在 Linux 中,目录名和文件名中包含空格当然是合法的。要适应这种情况,应该将 $file 变量用双引号圈起来。如果不这么做,遇到含有空格的目录名或文件名时就会有错误产生。

./test.sh: line 6: [: too many arguments
./test.sh: line 9: [: too many arguments

也可以在 for 命令中列出多个目录通配符,将目录查找和列表合并进同一个 for 语句。

#!/bin/bash

for file in "$HOME"/Documents/markdownNote/* "$HOME"/Desktop
do
if [ -d "$file" ]
then
echo "$file is a directory"
elif [ -f "$file" ]
then
echo "$file is a file"
fi
done

C 语言风格的 for 命令

#!/bin/bash
# testing the C-style for loop
for (( i=1; i <= 3; i++ ))
do
echo "The next number is $i"
done

C 语言风格的 for 命令也允许为迭代使用多个变量。循环会单独处理每个变量

#!/bin/bash
# multiple variables
for (( a=1, b=10; a <= 3; a++, b-- ))
do
echo "$a - $b"
done

循环输出

在 shell 脚本中,你可以对循环的输出使用管道或进行重定向。这可以通过在 done 命令之后添加一个处理命令来实现。

for file in /home/alsritter/*
do
if [ -d "$file" ]
then
echo "$file is a directory"
else
echo "$file is a file"
fi
done > output.txt

执行后可以发现,它把输出重定向到了 output.txt 文件里面了,同理可以使用管道符

# ...
done | sort

while 语句

while condition
do
command
done

以下是一个基本的 while 循环,测试条件是:如果 int 小于等于 5,那么条件返回真。int 从 1 开始,每次循环处理时,int 加 1。运行上述脚本,返回数字 1 到 5,然后终止。

#!/bin/bash
int=1
while(( $int<=5 ))
do
echo $int
let "int++"
done

跳出循环

break命令

#!/bin/bash
while :
do
echo -n "输入 1 到 5 之间的数字:"
read aNum
case $aNum in
1|2|3|4|5) echo "你输入的数字为 $aNum!"
;;
*) echo "你输入的数字不是 1 到 5 之间的! 游戏结束"
break
;;
esac
done

有时你在内部循环,但需要停止外部循环。break 命令接受单个命令行参数值:

break n

其中 n 指定了要跳出的循环层级。默认情况下,n 为 1,表明跳出的是当前的循环。如果你将 n 设为 2,break 命令就会停止下一级的外部循环。

e.g.

#!/bin/bash
# breaking out of an outer loop
for (( a = 1; a < 4; a++ ))
do
echo "Outer loop: $a"
for (( b = 1; b < 100; b++ ))
do
if [ $b -gt 4 ]
then
break 2
fi
echo " Inner loop: $b"
done
done

$ ./test.sh
Outer loop: 1
Inner loop: 1
Inner loop: 2
Inner loop: 3
Inner loop: 4
$

continue

#!/bin/bash
while :
do
echo -n "输入 1 到 5 之间的数字: "
read aNum
case $aNum in
1|2|3|4|5) echo "你输入的数字为 $aNum!"
;;
*) echo "你输入的数字不是 1 到 5 之间的!"
continue
echo "游戏结束"
;;
esac
done

和 break 命令一样,continue 命令也允许通过命令行参数指定要继续执行哪一级循环:

continue n

其中 n 定义了要继续的循环层级。

使用例子

查找可执行文件

如果你想找出系统中有哪些可执行文件可供使用,只需要扫描 PATH 环境变量中所有的目录就行了。

首先是创建一个 for 循环,对环境变量 PATH 中的目录进行迭代。处理的时候别忘了设置 IFS 分隔符。

IFS=:
for folder in $PATH
do

现在已经将各个目录存放在了变量 $folder 中,可以使用另一个 for 循环来迭代特定目录中的所有文件。

for file in $folder/*
do

最后一步是检查各个文件是否具有可执行权限,可以使用 if-then 测试功能来实现。

if [ -x $file ]
then
echo " $file"
fi

最后将这些代码片段组合成脚本就行了。

#!/bin/bash
# finding files in the PATH
IFS=:
for folder in $PATH
do
echo "$folder:"
for file in $folder/*
do
if [ -x $file ]
then
echo " $file"
fi
done
done

References

更多的结构化命令